package manager

import (
	"context"
	"errors"
	"happy-sudoku/graphql/models"
	"happy-sudoku/internal/model"
	"happy-sudoku/internal/util"
)

// !!TODO: The following code is verbose, find a way to solve it
var errRoomNotFound = errors.New("Room Not Found")

// IRoomSyncManager is use to implement subscription on room
// All subscription is split in room, and players
// Like map[room](subscribers(players))
type IRoomSyncManager interface {
	// Exist check whether the room exist
	Exist(ctx context.Context, roomID int) bool
	// InitAll init all pubsub
	InitAll(ctx context.Context, roomID int)
	// DestroyAll destroy all pubsub
	DestroyAll(ctx context.Context, roomID int)
	// SubscribeAll subscribe all pubsub
	SubscribeAll(ctx context.Context, roomID int, playerID int)
	// UnsubscribeAll unsubscribe all pubsub
	UnsubscribeAll(ctx context.Context, roomID int, playerID int)

	// InitPlayersRoomSync init pubsub for room
	InitPlayersRoomSync(ctx context.Context, roomID int)
	// SubscribePlayersToRoom add player subscriber to get room
	SubscribePlayersToRoom(ctx context.Context, roomID int, playerID int) <-chan *model.Room
	// UnsubscribePlayersToRoom remove player subscriber to player state
	UnsubscribePlayersToRoom(ctx context.Context, roomID int, playerID int)
	// PublishRoomToPlayers publish room to players
	PublishRoomToPlayers(ctx context.Context, roomID int, room model.Room)
	// GetRecvChanPlayersRoomSync get output channel
	GetRecvChanPlayersRoomSync(ctx context.Context, roomID int, playerID int) (<-chan *model.Room, bool)
	// DestroyPlayersRoomSync destroy pubsub for room
	DestroyPlayersRoomSync(ctx context.Context, roomID int)

	// InitPlayersStateSync init pubsub for PlayerState
	InitPlayersStateSync(ctx context.Context, roomID int)
	// SubscribePlayersToPlayerState add player subscriber to player state
	SubscribePlayersToPlayerState(ctx context.Context, roomID int, playerID int) <-chan *model.PlayerState
	// UnsubscribePlayersToPlayerState remove player subscriber to player state
	UnsubscribePlayersToPlayerState(ctx context.Context, roomID int, playerID int)
	// PublishPlayersStateToPlayers publish state to all subscribed player
	PublishPlayersStateToPlayers(ctx context.Context, roomID int, playerState model.PlayerState)
	// GetRecvChanPlayersStateSync get output channel
	GetRecvChanPlayersStateSync(ctx context.Context, roomID int, playerID int) (<-chan *model.PlayerState, bool)
	// DestroyPlayersStateSync destroy pubsub for PlayerState
	DestroyPlayersStateSync(ctx context.Context, roomID int)

	// InitPlayersGameTimeSync init pubsub for GameTime
	InitPlayersGameTimeSync(ctx context.Context, roomID int)
	// SubscribePlayersToGameTime add player subscriber to GameTime
	SubscribePlayersToGameTime(ctx context.Context, roomID int, playerID int) <-chan model.TimeDuration
	// UnsubscribePlayersToGameTime remove player subscriber to GameTime
	UnsubscribePlayersToGameTime(ctx context.Context, roomID int, playerID int)
	// PublishGameTimeToPlayers publish time to all subscribed player
	PublishGameTimeToPlayers(ctx context.Context, roomID int, remainTime model.TimeDuration)
	// GetRecvChanPlayersGameTimeSync get output channel
	GetRecvChanPlayersGameTimeSync(ctx context.Context, roomID int, playerID int) (<-chan model.TimeDuration, bool)
	// DestroyPlayersGameTimeSync destroy pubsub for GameTime
	DestroyPlayersGameTimeSync(ctx context.Context, roomID int)

	// SubscribeRoomListUpdated add player subscriber to updatedRoom
	SubscribeRoomListUpdated(ctx context.Context, playerID int) <-chan *models.RoomListUpdatedRes
	// UnsubscribeRoomListUpdated remove player subscriber to updatedRoom
	UnsubscribeRoomListUpdated(ctx context.Context, playerID int)
	// PublishRoomListUpdated publish updated Room to all subscribed player
	PublishRoomListUpdated(ctx context.Context, room models.RoomListUpdatedRes)
}

// RoomSyncManager implements IRoomSyncManager
type RoomSyncManager struct {
	roomSyncMap  util.SyncIntMapModelRoomPubSub
	stateSyncMap util.SyncIntMapModelPlayerStatePubSub
	timeSyncMap  util.SyncIntMapModelTimeDurationPubSub

	roomListUpdatedPubsub *util.ModelsRoomListUpdatedResPubSub
}

// NewRoomSyncManager new NoomSyncManager
func NewRoomSyncManager() *RoomSyncManager {
	return &RoomSyncManager{
		roomSyncMap:  util.MakeSyncIntMapModelRoomPubSub(),
		stateSyncMap: util.MakeSyncIntMapModelPlayerStatePubSub(),
		timeSyncMap:  util.MakeSyncIntMapModelTimeDurationPubSub(),

		roomListUpdatedPubsub: util.NewModelsRoomListUpdatedResPubSub(),
	}
}

// Exist check whether the room exist
func (r *RoomSyncManager) Exist(ctx context.Context, roomID int) bool {
	return r.roomSyncMap.Exist(roomID)
}

// InitAll init all pubsub
func (r *RoomSyncManager) InitAll(ctx context.Context, roomID int) {
	g := util.NewWaitGroup()

	g.Go(func() {
		r.InitPlayersRoomSync(ctx, roomID)
	})

	g.Go(func() {
		r.InitPlayersStateSync(ctx, roomID)
	})

	g.Go(func() {
		r.InitPlayersGameTimeSync(ctx, roomID)
	})

	g.Wait()
}

// DestroyAll destroy all pubsub
func (r *RoomSyncManager) DestroyAll(ctx context.Context, roomID int) {
	// UnsubscribeAll first

	g := util.NewWaitGroup()

	g.Go(func() {
		r.DestroyPlayersRoomSync(ctx, roomID)
	})

	g.Go(func() {
		r.DestroyPlayersStateSync(ctx, roomID)
	})

	g.Go(func() {
		r.DestroyPlayersGameTimeSync(ctx, roomID)
	})

	g.Wait()
}

// SubscribeAll subscribe all pubsub
func (r *RoomSyncManager) SubscribeAll(ctx context.Context, roomID int, playerID int) {
	g := util.NewWaitGroup()

	g.Go(func() {
		r.SubscribePlayersToRoom(ctx, roomID, playerID)
	})

	g.Go(func() {
		r.SubscribePlayersToPlayerState(ctx, roomID, playerID)
	})

	g.Go(func() {
		r.SubscribePlayersToGameTime(ctx, roomID, playerID)
	})

	g.Wait()
}

// UnsubscribeAll unsubscribe all pubsub
func (r *RoomSyncManager) UnsubscribeAll(ctx context.Context, roomID int, playerID int) {
	g := util.NewWaitGroup()

	g.Go(func() {
		r.UnsubscribePlayersToRoom(ctx, roomID, playerID)
	})

	g.Go(func() {
		r.UnsubscribePlayersToPlayerState(ctx, roomID, playerID)
	})

	g.Go(func() {
		r.UnsubscribePlayersToGameTime(ctx, roomID, playerID)
	})

	g.Wait()
}

// InitPlayersRoomSync init pubsub for room
func (r *RoomSyncManager) InitPlayersRoomSync(ctx context.Context, roomID int) {
	r.roomSyncMap.StoreOrLoad(roomID, util.NewModelRoomPubSub())
}

// SubscribePlayersToRoom add player subscriber to get room
func (r *RoomSyncManager) SubscribePlayersToRoom(ctx context.Context, roomID int, playerID int) <-chan *model.Room {
	return r.roomSyncMap.Load(roomID).Subscribe(playerID)
}

// UnsubscribePlayersToRoom remove player subscriber to player state
func (r *RoomSyncManager) UnsubscribePlayersToRoom(ctx context.Context, roomID int, playerID int) {
	r.roomSyncMap.Load(roomID).Unsubscribe(playerID)
}

// PublishRoomToPlayers publish room to players
func (r *RoomSyncManager) PublishRoomToPlayers(ctx context.Context, roomID int, room model.Room) {
	r.roomSyncMap.Load(roomID).Publish(&room)
}

// GetRecvChanPlayersRoomSync get output channel
func (r *RoomSyncManager) GetRecvChanPlayersRoomSync(ctx context.Context, roomID int, playerID int) (<-chan *model.Room, bool) {
	return r.roomSyncMap.Load(roomID).Get(playerID)
}

// DestroyPlayersRoomSync destroy pubsub for room
func (r *RoomSyncManager) DestroyPlayersRoomSync(ctx context.Context, roomID int) {
	pb, ok := r.roomSyncMap.LoadOk(roomID)
	if !ok {
		return
	}
	pb.UnsubscribeAll()
	r.roomSyncMap.Delete(roomID)
}

// InitPlayersStateSync init pubsub for PlayerState
func (r *RoomSyncManager) InitPlayersStateSync(ctx context.Context, roomID int) {
	r.stateSyncMap.StoreOrLoad(roomID, util.NewModelPlayerStatePubSub())
}

// SubscribePlayersToPlayerState add player subscriber to player state
func (r *RoomSyncManager) SubscribePlayersToPlayerState(ctx context.Context, roomID int, playerID int) <-chan *model.PlayerState {
	return r.stateSyncMap.Load(roomID).Subscribe(playerID)
}

// UnsubscribePlayersToPlayerState remove player subscriber to player state
func (r *RoomSyncManager) UnsubscribePlayersToPlayerState(ctx context.Context, roomID int, playerID int) {
	r.stateSyncMap.Load(roomID).Unsubscribe(playerID)
}

// PublishPlayersStateToPlayers publish state to all subscribed player
func (r *RoomSyncManager) PublishPlayersStateToPlayers(ctx context.Context, roomID int, playerState model.PlayerState) {
	r.stateSyncMap.Load(roomID).Publish(&playerState)
}

// GetRecvChanPlayersStateSync get output channel
func (r *RoomSyncManager) GetRecvChanPlayersStateSync(ctx context.Context, roomID int, playerID int) (<-chan *model.PlayerState, bool) {
	return r.stateSyncMap.Load(roomID).Get(playerID)
}

// DestroyPlayersStateSync destroy pubsub for PlayerState
func (r *RoomSyncManager) DestroyPlayersStateSync(ctx context.Context, roomID int) {
	pb, ok := r.stateSyncMap.LoadOk(roomID)
	if !ok {
		return
	}
	pb.UnsubscribeAll()
	r.stateSyncMap.Delete(roomID)
}

// InitPlayersGameTimeSync init pubsub for GameTime
func (r *RoomSyncManager) InitPlayersGameTimeSync(ctx context.Context, roomID int) {
	r.timeSyncMap.StoreOrLoad(roomID, util.NewModelTimeDurationPubSub())
}

// SubscribePlayersToGameTime add player subscriber to GameTime
func (r *RoomSyncManager) SubscribePlayersToGameTime(ctx context.Context, roomID int, playerID int) <-chan model.TimeDuration {
	return r.timeSyncMap.Load(roomID).Subscribe(playerID)
}

// UnsubscribePlayersToGameTime remove player subscriber to GameTime
func (r *RoomSyncManager) UnsubscribePlayersToGameTime(ctx context.Context, roomID int, playerID int) {
	r.timeSyncMap.Load(roomID).Unsubscribe(playerID)
}

// PublishGameTimeToPlayers publish state to all subscribed player
func (r *RoomSyncManager) PublishGameTimeToPlayers(ctx context.Context, roomID int, remainTime model.TimeDuration) {
	r.timeSyncMap.Load(roomID).Publish(remainTime)
}

// GetRecvChanPlayersGameTimeSync get output channel
func (r *RoomSyncManager) GetRecvChanPlayersGameTimeSync(ctx context.Context, roomID int, playerID int) (<-chan model.TimeDuration, bool) {
	return r.timeSyncMap.Load(roomID).Get(playerID)
}

// DestroyPlayersGameTimeSync destroy pubsub for GameTime
func (r *RoomSyncManager) DestroyPlayersGameTimeSync(ctx context.Context, roomID int) {
	pb, ok := r.timeSyncMap.LoadOk(roomID)
	if !ok {
		return
	}
	pb.UnsubscribeAll()
	r.timeSyncMap.Delete(roomID)
}

// SubscribeRoomListUpdated add player subscriber to updatedRoom
func (r *RoomSyncManager) SubscribeRoomListUpdated(ctx context.Context, playerID int) <-chan *models.RoomListUpdatedRes {
	return r.roomListUpdatedPubsub.Subscribe(playerID)
}

// UnsubscribeRoomListUpdated remove player subscriber to updatedRoom
func (r *RoomSyncManager) UnsubscribeRoomListUpdated(ctx context.Context, playerID int) {
	r.roomListUpdatedPubsub.Unsubscribe(playerID)
}

// PublishRoomListUpdated publish updated Room to all subscribed player
func (r *RoomSyncManager) PublishRoomListUpdated(ctx context.Context, room models.RoomListUpdatedRes) {
	r.roomListUpdatedPubsub.Publish(&room)
}
